namespace NovellSecurity
{
  using System;
  using System.Diagnostics;
  using System.DirectoryServices.Protocols;
  using System.Net;
  using System.Web.Security;
  using NovellSecurity.Caching;
  using Sitecore.Caching;
  using Sitecore.Diagnostics;
  using System.Configuration;
  using System.Text;
  using Sitecore.Configuration;
  using Sitecore;

  /// <summary>
  /// The membership provider.
  /// </summary>
  public class MembershipProvider : System.Web.Security.MembershipProvider
  {
    protected LdapConnection Connection { get; set; }
    protected bool IsInitilized { get; set; }
    protected DateTime CommonDate { get; set; }
    protected string BaseDn { get; set; }
    protected string Filter { get; set; }
    protected string eDirAttrUserName { get; set; }
    protected string eDirUserUniqueId { get; set; }
    protected string eDirEmailAttribute { get; set; }
    private TimeSpan UserConnectionTimeout {get; set; }
       
    /// <summary>
    /// Contstructor
    /// </summary>
    public MembershipProvider()
    {
      this.CommonDate = DateTime.Now;
    }

    /// <summary>
    /// Gets or sets the application name.
    /// </summary>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override string ApplicationName
    {
      get
      {
        return "Sitecore";
      }
      set { }
    }

    public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
    {
      base.Initialize(name, config);
      this.IsInitilized = false;
      this.BaseDn = config["BaseDn"];
      this.Filter = config["filter"];
      this.eDirAttrUserName = config["userName"];
      this.eDirUserUniqueId = config["uniqueId"];
      this.eDirEmailAttribute = config["emailAttribute"];
      this.UserConnectionTimeout = Settings.GetTimeSpanSetting("Novell.Users.RequestTimeout", "00:02:00");
      this.Connection = ConnectionHelper.Connect(config["connectionStringName"], config["connectionUserDn"], config["connectionPassword"]);
      try
      { 
        this.Connection.Bind();
        this.IsInitilized = true;
      }
      catch (Exception e)
      {
        Log.Error("Novell eDirectory: troubles with the connection. Details: "+e.Message, this);
      }
    }
    /// <summary>
    /// The change password.
    /// </summary>
    /// <param name="username">
    /// The username.
    /// </param>
    /// <param name="oldPassword">
    /// The old password.
    /// </param>
    /// <param name="newPassword">
    /// The new password.
    /// </param>
    /// <returns>
    /// The <see cref="bool"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override bool ChangePassword(string username, string oldPassword, string newPassword)
    {
      Log.Debug("Novell eDirectory: Method invoked: ChangePassword is not implemented", this);
      return false;
    }

    /// <summary>
    /// The change password question and answer.
    /// </summary>
    /// <param name="username">
    /// The username.
    /// </param>
    /// <param name="password">
    /// The password.
    /// </param>
    /// <param name="newPasswordQuestion">
    /// The new password question.
    /// </param>
    /// <param name="newPasswordAnswer">
    /// The new password answer.
    /// </param>
    /// <returns>
    /// The <see cref="bool"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
    {
      Log.Debug("Novell eDirectory: Method invoked: ChangePasswordQuestionAndAnswer is not implemented", this);
      return false;
    }

    /// <summary>
    /// The create user.
    /// </summary>
    /// <param name="username">
    /// The username.
    /// </param>
    /// <param name="password">
    /// The password.
    /// </param>
    /// <param name="email">
    /// The email.
    /// </param>
    /// <param name="passwordQuestion">
    /// The password question.
    /// </param>
    /// <param name="passwordAnswer">
    /// The password answer.
    /// </param>
    /// <param name="isApproved">
    /// The is approved.
    /// </param>
    /// <param name="providerUserKey">
    /// The provider user key.
    /// </param>
    /// <param name="status">
    /// The status.
    /// </param>
    /// <returns>
    /// The <see cref="MembershipUser"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
    {
      Log.Debug("Novell eDirectory: Method invoked: CreateUser is not implemented", this);
      status = new MembershipCreateStatus();
      return null;
    }

    /// <summary>
    /// The delete user.
    /// </summary>
    /// <param name="username">
    /// The username.
    /// </param>
    /// <param name="deleteAllRelatedData">
    /// The delete all related data.
    /// </param>
    /// <returns>
    /// The <see cref="bool"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override bool DeleteUser(string username, bool deleteAllRelatedData)
    {
      Log.Debug("Novell eDirectory: Method invoked: DeleteUser is not implemented", this);
      return false;
    }

    /// <summary>
    /// Gets a value indicating whether enable password reset.
    /// </summary>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override bool EnablePasswordReset
    {
      get
      {
        return false;
      }
    }

    /// <summary>
    /// Gets a value indicating whether enable password retrieval.
    /// </summary>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override bool EnablePasswordRetrieval
    {
      get
      {
        return false;
      }
    }

    /// <summary>
    /// The find users by email.
    /// </summary>
    /// <param name="emailToMatch">
    /// The email to match.
    /// </param>
    /// <param name="pageIndex">
    /// The page index.
    /// </param>
    /// <param name="pageSize">
    /// The page size.
    /// </param>
    /// <param name="totalRecords">
    /// The total records.
    /// </param>
    /// <returns>
    /// The <see cref="MembershipUserCollection"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
    {
      Log.Debug("Novell eDirectory: Method invoked: FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)", this);
      MembershipUserCollection allUsers = new MembershipUserCollection();
      MembershipUserCollection foundUsers = new MembershipUserCollection();
      if (NovellCacheManager.IsCacheEmptyOrNull("NovellUsersCache"))
      {
        this.FillUsersCacheFromEdir();
      }
      allUsers = NovellCacheManager.GetAllUsers();
      string emailToFind = emailToMatch.ToLower().Replace("*", "");
      foreach (NovellUser currentUser in allUsers)
      {
        if (currentUser.Email.ToLower().Contains(emailToFind))
        {
          foundUsers.Add(currentUser);      
        }
        if (foundUsers.Count == int.Parse(Settings.GetSetting("Novell.Users.FindUsersLimitt")))
        {
          break;
        }
      }
      totalRecords = foundUsers.Count;
      foundUsers = this.ComposeUsersPage(foundUsers, pageIndex, pageSize);
      return foundUsers;
    }

    /// <summary>
    /// The find users by name.
    /// </summary>
    /// <param name="usernameToMatch">
    /// The username to match.
    /// </param>
    /// <param name="pageIndex">
    /// The page index.
    /// </param>
    /// <param name="pageSize">
    /// The page size.
    /// </param>
    /// <param name="totalRecords">
    /// The total records.
    /// </param>
    /// <returns>
    /// The <see cref="MembershipUserCollection"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
    {
      Log.Debug("Novell eDirectory: Method invoked: FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)", this);
      MembershipUserCollection allUsers = new MembershipUserCollection();
      MembershipUserCollection foundUsers = new MembershipUserCollection();
      if (NovellCacheManager.IsCacheEmptyOrNull("NovellUsersCache"))
      {
        this.FillUsersCacheFromEdir();
      }
      allUsers = NovellCacheManager.GetAllUsers();
      string usernameToFind = usernameToMatch.ToLower().Replace("*", "");
      foreach(NovellUser currentUser in allUsers)
      {
        if(currentUser.UserName.ToLower().Contains(usernameToFind))
        {
          foundUsers.Add(currentUser);         
        }
        if (foundUsers.Count == int.Parse(Settings.GetSetting("Novell.Users.FindUsersLimit")))
        {
          break;
        }
        
      }
      totalRecords = foundUsers.Count;
      foundUsers = this.ComposeUsersPage(foundUsers, pageIndex, pageSize);
      return foundUsers;
    }

    /// <summary>
    /// The get all users.
    /// </summary>
    /// <param name="pageIndex">
    /// The page index.
    /// </param>
    /// <param name="pageSize">
    /// The page size.
    /// </param>
    /// <param name="totalRecords">
    /// The total records.
    /// </param>
    /// <returns>
    /// The <see cref="MembershipUserCollection"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    /// 
    public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
    {
      Log.Debug("Novell eDirectory: Method invoked: GetAllUsers(int pageIndex, int pageSize, out int totalRecords)", this);
      MembershipUserCollection users = new MembershipUserCollection();
      if (NovellCacheManager.IsCacheEmptyOrNull("NovellUsersCache"))
      {
        this.FillUsersCacheFromEdir();
      }
      users = NovellCacheManager.GetAllUsers();
      totalRecords = users.Count;
      users = this.ComposeUsersPage(users,pageIndex,pageSize);
      return users;
    }

    /// <summary>
    /// The get number of users online.
    /// </summary>
    /// <returns>
    /// The <see cref="int"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override int GetNumberOfUsersOnline()
    {
      Log.Debug("Novell eDirectory: Method invoked: GetNumberOfUsersOnline is not implemented", this);
      return -1;
    }

    /// <summary>
    /// The get password.
    /// </summary>
    /// <param name="username">
    /// The username.
    /// </param>
    /// <param name="answer">
    /// The answer.
    /// </param>
    /// <returns>
    /// The <see cref="string"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override string GetPassword(string username, string answer)
    {
      Log.Debug("Novell eDirectory: Method invoked: GetPassword is not implemented", this);
      return "";
    }

    /// <summary>
    /// The get user.
    /// </summary>
    /// <param name="username">
    /// The username.
    /// </param>
    /// <param name="userIsOnline">
    /// The user is online.
    /// </param>
    /// <returns>
    /// The <see cref="MembershipUser"/>.
    /// </returns>
    public override MembershipUser GetUser(string username, bool userIsOnline)
    {
      Log.Debug("Novell eDirectory: Method invoked: GetUser(string username, bool userIsOnline)", this);
      if (NovellCacheManager.GetUser(username) == null)
      {
        this.FillUsersCacheFromEdir();
      }
      NovellUser currentUser = NovellCacheManager.GetUser(username);
      if(currentUser == null)
      {
        Log.Debug("Novell eDirectory:  The user with the name " + username + " is not found", this);
        return null;
      }
      return currentUser;
    }

    /// <summary>
    /// The get user.
    /// </summary>
    /// <param name="providerUserKey">
    /// The provider user key.
    /// </param>
    /// <param name="userIsOnline">
    /// The user is online.
    /// </param>
    /// <returns>
    /// The <see cref="MembershipUser"/>.
    /// </returns>
    public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
    {
      Log.Debug("Novell eDirectory: Method invoked: GetUser(object providerUserKey, bool userIsOnline) is not implemented", this);
      return null;
    }

    /// <summary>
    /// The get user name by email.
    /// </summary>
    /// <param name="email">
    /// The email.
    /// </param>
    /// <returns>
    /// The <see cref="string"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override string GetUserNameByEmail(string email)
    {
      Log.Debug("Novell eDirectory: Method invoked:  GetUserNameByEmail(string email) is not implemented", this);
      return "";
    }

    /// <summary>
    /// Gets the max invalid password attempts.
    /// </summary>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override int MaxInvalidPasswordAttempts
    {
      get
      {
        return 0;
      }
    }

    /// <summary>
    /// Gets the min required non alphanumeric characters.
    /// </summary>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override int MinRequiredNonAlphanumericCharacters
    {
      get
      {
        return 0;
      }
    }

    /// <summary>
    /// Gets the min required password length.
    /// </summary>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override int MinRequiredPasswordLength
    {
      get
      {
        return 0;
      }
    }

    /// <summary>
    /// Gets the password attempt window.
    /// </summary>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override int PasswordAttemptWindow
    {
      get
      {
        return 0;
      }
    }

    /// <summary>
    /// Gets the password format.
    /// </summary>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override MembershipPasswordFormat PasswordFormat
    {
      get
      {
        return 0;
      }
    }

    /// <summary>
    /// Gets the password strength regular expression.
    /// </summary>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override string PasswordStrengthRegularExpression
    {
      get
      {
        return "";
      }
    }

    /// <summary>
    /// Gets a value indicating whether requires question and answer.
    /// </summary>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override bool RequiresQuestionAndAnswer
    {
      get
      {
        return false;
      }
    }

    /// <summary>
    /// Gets a value indicating whether requires unique email.
    /// </summary>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override bool RequiresUniqueEmail
    {
      get
      {
        return false;
      }
    }

    /// <summary>
    /// The reset password.
    /// </summary>
    /// <param name="username">
    /// The username.
    /// </param>
    /// <param name="answer">
    /// The answer.
    /// </param>
    /// <returns>
    /// The <see cref="string"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override string ResetPassword(string username, string answer)
    {
      Log.Debug("Novell eDirectory: Method invoked: Reset Password ios not implemented", this);
      return "";
    }

    /// <summary>
    /// The unlock user.
    /// </summary>
    /// <param name="userName">
    /// The user name.
    /// </param>
    /// <returns>
    /// The <see cref="bool"/>.
    /// </returns>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override bool UnlockUser(string userName)
    {
      Log.Debug("Novell eDirectory: Method invoked: UnlockUser is not implemented", this);
      return false;
    }

    /// <summary>
    /// The update user.
    /// </summary>
    /// <param name="user">
    /// The user.
    /// </param>
    /// <exception cref="NotImplementedException">
    /// </exception>
    public override void UpdateUser(MembershipUser user)
    {
      Log.Debug("Novell eDirectory: Method invoked:  UpdateUser(MembershipUser user) is not implemented", this);
    }

    /// <summary>
    /// The validate user.
    /// </summary>
    /// <param name="username">
    /// The username.
    /// </param>
    /// <param name="password">
    /// The password.
    /// </param>
    /// <returns>
    /// The <see cref="bool"/>.
    /// </returns>
    public override bool ValidateUser(string username, string password)
    {
      Log.Debug("Novell eDirectory: Method invoked:  ValidateUser(string username, string password)", this);
      if (this.IsInitilized)
      {
        SearchRequest request = new SearchRequest(this.BaseDn, "(&(" + this.Filter + ")(" + this.eDirAttrUserName + "=" + username + "))", System.DirectoryServices.Protocols.SearchScope.Subtree, this.eDirAttrUserName);
        SearchResponse response = (SearchResponse)this.Connection.SendRequest(request);
        if (response.Entries.Count > 0)
        {
          try
          {
            NetworkCredential nc = new NetworkCredential(response.Entries[0].DistinguishedName, password);
            this.Connection.Bind(nc);
            return true;
          }
          catch (Exception exc)
          {
            Log.Info("Novell eDirectory: login failed. Details: " + exc.Message + " || " + exc.StackTrace, this); // "Login failed for user"+username
            return false;
          }
        }
        else
        {
          Log.Info("Novell eDirectory:  No users found", this);
          return false;
        }
      }
      return false;
    }

    /// <summary>
    /// Composes the users page.
    /// </summary>
    /// <param name="allUsers">All users.</param>
    /// <param name="pageIndex">Index of the page.</param>
    /// <param name="pageSize">Size of the page.</param>
    /// <returns></returns>
    protected virtual MembershipUserCollection ComposeUsersPage(MembershipUserCollection allUsers, int pageIndex, int pageSize)
    {
      MembershipUserCollection users = new MembershipUserCollection();
      int currentUser = 0;
      foreach (MembershipUser user in allUsers)
      {
        if ((currentUser >= pageIndex * pageSize) && (currentUser < (pageIndex * pageSize + pageSize)))
        {
          users.Add(user);
        }
        currentUser++;
      }
      return users;
    }

    /// <summary>
    /// Adds the difference user unique.
    /// </summary>
    /// <param name="users">The users.</param>
    /// <param name="newUser">The new user.</param>
    /// <returns></returns>
    protected virtual MembershipUserCollection AddIfUserUnique(MembershipUserCollection users, MembershipUser newUser)
    {
      foreach (MembershipUser user in users)
      {
        if (newUser.UserName == user.UserName)
        {
          Log.Warn("Novell eDirectory: The user name " + newUser + " is not unique. Please try to use another property for mapping the users (insted of " + this.eDirAttrUserName + ") or change the user " + this.eDirAttrUserName + " on the eDir side.", this);
          return users;
        }
      }
      users.Add(newUser);
      return users;
    }

    /// <summary>
    /// Fills the users cache from edir.
    /// </summary>
    public virtual void FillUsersCacheFromEdir()
    {
      if (this.IsInitilized)
      {
        MembershipUserCollection users = new MembershipUserCollection();
        try
        {
          SearchRequest request = new SearchRequest(this.BaseDn, "(&(" + this.Filter + "))", System.DirectoryServices.Protocols.SearchScope.Subtree, this.eDirAttrUserName, this.eDirUserUniqueId, this.eDirEmailAttribute);
          SearchResponse response;
          request.SizeLimit = int.Parse(Settings.GetSetting("Novell.Users.MaxSizeLimit"));
          try
          {
            response = (SearchResponse)this.Connection.SendRequest(request, this.UserConnectionTimeout);
          }
          catch (DirectoryOperationException e)
          {
            response = (SearchResponse)e.Response;
          }
          if (response.Entries.Count > 0)
          {
            Log.Debug("Novell eDirectory:  Users count in eDir=" + response.Entries.Count, this);
            int currentUser = 0;
            foreach (SearchResultEntry ADuser in response.Entries)
            {
              string userName = ADuser.Attributes[this.eDirAttrUserName] != null ? ADuser.Attributes[this.eDirAttrUserName][0].ToString() : "";
              string userGuid = ADuser.Attributes[this.eDirUserUniqueId] != null ? Encoding.Default.GetString((Byte[])ADuser.Attributes[this.eDirUserUniqueId][0]) : "";
              string userEmail = ADuser.Attributes[this.eDirEmailAttribute] != null ? ADuser.Attributes[this.eDirEmailAttribute][0].ToString() : "";
              Assert.ArgumentNotNullOrEmpty(userName, "user name for a Novell Edirectory user");
              Assert.ArgumentNotNullOrEmpty(userGuid, "user Unique Guid for a Novell Edirectory user");
              Log.Debug("Novell eDirectory: Current User name=" + userName + "; User name=" + userName + "; GUID=" + userGuid.ToString(), this);
              NovellUser tempUser = new NovellUser(userName.ToLower(), userGuid.ToString(), userEmail, this.Name);
              users = this.AddIfUserUnique(users, tempUser);
              currentUser++;
            }
            this.ClearCacheAndFillUsers(users);
          }
          else
          {
            Log.Info("Novell eDirectory:  No users are found", this);
          }
        }
        catch (Exception exc)
        {
          Log.Error("Novell eDirectory: " + exc.Message + " || " + exc.StackTrace + " || " + exc.Source, this);
        }
      }
    }

    /// <summary>
    /// Clears the cache and fill users.
    /// </summary>
    /// <param name="users">The users.</param>
    protected virtual void ClearCacheAndFillUsers(MembershipUserCollection users)
    {
      NovellCacheManager.Clear("NovellUsersCache");
      Log.Debug("Novell eDirectory: Membership cached is cleared", this);
      Log.Debug("Novell eDirectory: Membership cached is filling up", this);
      foreach (MembershipUser user in users)
      {
        NovellCacheManager.AddUser((NovellUser)user);
      }
      if (!NovellCacheManager.IsCacheEmptyOrNull("NovellUsersCache"))
      {
        Log.Debug("Novell eDirectory: Count of cache values " + CacheManager.FindCacheByName("NovellUsersCache").Count, this);
      }
    }

    /// <summary>
    /// Gets the user from edir and fill cache.
    /// </summary>
    /// <param name="username">The username.</param>
    protected virtual void GetUserFromEdirAndFillCache(string username)
    {
      if (this.IsInitilized)
      {
        MembershipUserCollection users = new MembershipUserCollection();
        SearchRequest request = new SearchRequest(this.BaseDn, "(&(" + this.Filter + ")(" + this.eDirAttrUserName + "=" + username + "))", System.DirectoryServices.Protocols.SearchScope.Subtree, this.eDirAttrUserName, this.eDirUserUniqueId);
        SearchResponse response = (SearchResponse)this.Connection.SendRequest(request);
        if (response.Entries.Count > 0)
        {
          string userName = response.Entries[0].Attributes[this.eDirAttrUserName] != null ? response.Entries[0].Attributes[this.eDirAttrUserName][0].ToString() : "";
          string userGuid = response.Entries[0].Attributes[this.eDirUserUniqueId] != null ? Encoding.Default.GetString((Byte[])response.Entries[0].Attributes[this.eDirUserUniqueId][0]) : "";
          string userEmail = response.Entries[0].Attributes[this.eDirEmailAttribute] != null ? response.Entries[0].Attributes[this.eDirEmailAttribute][0].ToString() : "";
          Assert.ArgumentNotNullOrEmpty(userName, "user name for a Novell Edirectory user");
          Assert.ArgumentNotNullOrEmpty(userGuid, "user Unique Guid for a Novell Edirectory user");
          NovellUser tempUser = new NovellUser(userName.ToLower(), userGuid, userEmail, this.Name);
          if (!(NovellCacheManager.GetUser(username) != null))
          {
            NovellCacheManager.AddUser(tempUser);
          }
        }
      }
    }
  }
}